package model;

import java.io.File;
import java.sql.SQLException;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import view.GUI.mainView.KindVBox.BoxType;
import controller.Controller;
import enums.Message;
import enums.PaymentType;
import enums.Property;
import enums.Status;
import model.data.interfaces.IFinishable;
import model.data.tableModel.TableControl;
import model.data.tableModel.inventory.Inventory;
import model.data.tableModel.person.Customer;
import model.data.tableModel.person.Staff;
import model.data.tableModel.sale.Bill;
import model.data.tableModel.sale.Product;
import model.data.wrapper.InputWrapper;
import model.data.wrapper.ToPay;
import model.handler.databaseHandling.LogInOutHandler;
import model.handler.databaseHandling.QueryFactory;
import model.handler.databaseHandling.QueryFactory.QueryProperty;
import model.handler.emailHandling.EmailHandler;
import model.handler.fileHandling.FileHandler;
import model.handler.fileHandling.PdfHandler;
import model.invoicing.DateHandler;
import model.invoicing.InvoiceHandler;

/**
 * Central control of the model.
 * <p>
 * Handles the whole model and delegates commands to the controller.
 */
public final class Control {

	private static Logger debug = LogManager.getLogger("DEBUG");

	private final LogInOutHandler logInOutHandler;
	private final Controller controller;
	private final TableControl tableControl;

	/**
	 * Instance of this class.
	 */
	private static Control instance;

	/**
	 * Private constructor because this class is implemented as Singleton.
	 */
	private Control() {
		controller = Controller.getInstance();
		logInOutHandler = LogInOutHandler.getInstance(this);
		tableControl = TableControl.getInstance();
	}

	/**
	 * @return Instance of this class because of the SingleTon-Pattern
	 */
	public static Control getInstance() {
		if (instance == null) {
			instance = new Control();
		}
		return instance;
	}

	/**
	 * Creates the .pdf files, invoice for customers as well as the balance and
	 * the salary invoice. Stores the created files to the database and sends
	 * them to the recipient.
	 * 
	 * @param password Password of the email account.
	 * @throws IllegalArgumentException if the password is wrong
	 * @throws SQLException
	 */
	public void createInvoice(String password) throws IllegalArgumentException, SQLException {
		if (logInOutHandler.checkEquality(password, QueryFactory.getProperty(Property.PASSWORDEMAIL), QueryFactory.getProperty(Property.SALTEMAIL))) {
			Staff operator = TableControl.getInstance().getStaffMap().get(Configuration.ADMINNAME);
			String houseOfficialMail = tableControl.getCustomerMap().get("Blaues Haus").getPerson().getEmail();
			Date invoiceDate = DateHandler.getMonthBeforeDate(new Date());
			String strInvoiceDate = DateHandler.formatMonthDateToString(invoiceDate);

			File salaryFile = createSalaryInvoice(houseOfficialMail, invoiceDate);
			// invProgress.setProgress(0.2, "Erstellen der Monatsbilanz...");
			File balanceFile = createBalanceInvoice(houseOfficialMail, operator, invoiceDate);
			QueryFactory.writeInvoiceToDatabase(TableControl.getCentralCounter(), strInvoiceDate, salaryFile, balanceFile);

			createCustomerInvoice(houseOfficialMail, operator, invoiceDate, password);

			// TODO change email address
			// invProgress.setProgress(0.9, "Sending E-Mails...");
			// sendInvoiceEmail("matthiasikg@gmx.net", operator, password,
			// strInvoiceDate);
			sendBalanceMail(houseOfficialMail, strInvoiceDate, password);
			// invProgress.setProgress(1, "Finished");
			debug.debug("Invoices created");
		} else {
			throw new IllegalArgumentException(Message.WRONGPW.toString());
		}
	}

	/**
	 * Forwards a login attempt to the LoginOutHandler
	 * 
	 * @param nick Nickname that wants to log in
	 * @param password entered password
	 * @param cash the cash at the beginning
	 * @param coins the coins at the end
	 */
	public void tryLogin(String nick, String password, String cash, String coins) {
		logInOutHandler.tryLogin(nick, password, cash, coins);
	}

	/**
	 * Forwards a logout attempt to the LogInOutHandler
	 * 
	 * @param cash the cash at the end
	 * @param coins Coins at the end
	 */
	public void tryLogout(String cash, String coins) {
		logInOutHandler.tryLogout(cash, coins);
	}

	/**
	 * Forwards the notification of a failed login attempt to the controller
	 * 
	 * @param msg Message containing the reason why the login failed
	 */
	public void loginFailed(Message msg) {
		controller.loginFailed(msg);
	}

	/**
	 * Forwards the notification of a successful login attempt to the controller
	 * 
	 * @param isAdmin If the user is admin or not
	 */
	public void loginSuccess(boolean isAdmin) {
		controller.loginSuccess(isAdmin);
	}

	/**
	 * Forwards a successful logout attempt to the controller
	 */
	public void logOutSuccess() {
		controller.logOutSuccess();
	}

	public void logoutFailed(Message msg) {
		controller.logoutFailed(msg);
	}

	public Product getProduct(String newValue) {
		return tableControl.getProduct(newValue);
	}

	public ToPay computeInputValue(Map<BoxType, List<InputWrapper>> inputMap) {
		return tableControl.computeInputValue(inputMap);
	}

	public boolean checkIfOldPWEquals(String value) {
		Staff workingStaff = tableControl.getActualSalesday().getStaff();
		return logInOutHandler.checkEquality(value, workingStaff.getPassword(), workingStaff.getSalt());
	}

	public boolean setNewPassword(Staff staff, String newPw) {
		return logInOutHandler.setNewPw(staff, newPw);
	}

	/**
	 * Updates the stock of the products and the trayCount and writes the
	 * updated to the database
	 * 
	 * @param order List of Products that were ordered
	 */
	public void takeOrder(Map<BoxType, List<InputWrapper>> order) {
		for (List<InputWrapper> l : order.values()) {
			for (InputWrapper i : l) {
				Product p = i.getSelectedProduct();
				int oldStock = p.getStock();
				int multiplicator = 1;
				if (p.getDeposit() != null) {
					multiplicator = p.getDeposit().getMultiplicator();
				}
				p.addStock(multiplicator * i.getTrayCount() + i.getBottleCount());
				p.setTrayCount(p.getTrayCount() + i.getTrayCount());
				p.writeUpdateToDB();

				debug.debug(Utility.concatStrings("Stock of product ", p.getName(), " has changed from ", String.valueOf(oldStock), " to ",
						String.valueOf(p.getStock())));
			}
		}
	}

	public boolean checkIfBillIsPaid(Customer customer) {
		if (QueryFactory.getActiveDebt(customer, DateHandler.getMonthBeforeDate(DateHandler.getMonthBeforeDate(new Date()))) == null) {
			return true;
		}
		return false;
	}

	/**
	 * @param inputList
	 * @param inventory
	 */
	public void createInventoryInvoice(List<List<InputWrapper>> inputList, Inventory inventory) {
		String tex = InvoiceHandler.createInventoryInvoice(inputList, this, inventory);

		File pdf = PdfHandler.createPdf(FileHandler.createTexFile(Configuration.PATHTEXINVOICECUSTOMER + "Inventur_" + inventory.getDate() + ".tex",
				tex));
		pdf.deleteOnExit();
		inventory.setFile(pdf);
		inventory.writeToDB();
		FileHandler.openPdf(pdf);

		debug.debug("Inventory invoice created");
	}

	/**
	 * Computes the loss of every product and writes it to the given
	 * StringBuilder.
	 * <p>
	 * Calculates the loss of every product. If a product has replacements and
	 * there is a loss, then the stock of the first product in the line of the
	 * replacements gets the difference as loss. It also calculates the loss of
	 * empty trays per product.
	 * 
	 * @param sb StringBuilder that is writing the .tex code
	 * @param inputList List of all InputWrapper containing the information of
	 *            the inventory
	 * @return Total loss
	 */
	public double computeLossEachProduct(StringBuilder sb, List<List<InputWrapper>> inputList) {
		double loss = 0.0;
		int theoreticalTrayCount = 0;
		int actualTrayCount = 0;

		Product emptyTray = tableControl.getProductMap().get(Configuration.EMPTYTRAY);
		for (List<InputWrapper> list : inputList) {
			for (InputWrapper i : list) {
				Product prod = i.getSelectedProduct();
				if (!prod.equals(emptyTray)) {

					int multiplicator = prod.getMultiplicator();
					if (prod.getDeposit() != null) {
						multiplicator = prod.getDeposit().getMultiplicator();
					}
					int actualStock = i.getBottleCount() + i.getTrayCount() * multiplicator;
					loss += calculateSingleProductLoss(sb, prod, actualStock);

					theoreticalTrayCount += prod.getAllTrays();
					actualTrayCount += i.getTrayCount();

					// If the theoretical value of trays of this product is not
					// equal to the actual
					// value of the trays of this product
					// then divide the actual number of trays among the product
					// and its replacements
					if (prod.getAllTrays() != i.getTrayCount()) {
						prod.setAllTrayCount(i.getTrayCount());
					}
				} else {
					theoreticalTrayCount += emptyTray.getStock();
					actualTrayCount += i.getTrayCount();
				}
			}
		}
		double lossEmptyTray = (actualTrayCount - theoreticalTrayCount) * emptyTray.getCost();
		sb.append(InvoiceHandler.getInventoryProduct(actualTrayCount, theoreticalTrayCount, emptyTray.getName(), emptyTray.getCost(), lossEmptyTray));
		loss += lossEmptyTray;

		return loss;
	}

	/*
	 * HELPER METHODS BELOW
	 */

	private void sendBalanceMail(String recipientMail, String dateOfInvoiceToSend, String enteredPassword) {
		File balanceInvoice = QueryFactory.getInvoice(QueryProperty.BALANCEINVOICE, dateOfInvoiceToSend);
		File salaryInvoice = QueryFactory.getInvoice(QueryProperty.SALARYINVOICE, dateOfInvoiceToSend);
		String username = QueryFactory.getProperty(Property.USERNAMEEMAIL);

		EmailHandler.sendBalanceMail(username, enteredPassword, recipientMail, balanceInvoice, salaryInvoice,
				DateHandler.formatHeaderDate(DateHandler.parseMonthDateStringToDate(dateOfInvoiceToSend)));
	}

	private File createBalanceInvoice(String houseOfficialMail, Staff operator, Date invoiceDate) {
		List<Integer> billIDs = new ArrayList<>();
		for (Customer c : tableControl.getCustomerMap().values()) {
			billIDs.add(QueryFactory.getBillID(c.getCustomerID(), DateHandler.formatMonthDateToString(invoiceDate)));
		}

		File pdf = PdfHandler.createPdf(FileHandler.createTexFile(
				Configuration.PATHTEXINVOICECUSTOMER + DateHandler.formatMonthDateToString(DateHandler.getMonthBeforeDate(new Date()))
						+ "_Monatsbilanz.tex", InvoiceHandler.createTexBalanceInvoice(billIDs, operator)));
		pdf.deleteOnExit();

		return pdf;
	}

	/**
	 * Creates the salary invoice as .pdf file and writes it to the database.
	 * 
	 * @param houseOfficialMail email address of the "Haussprecher"
	 * @param invoiceDate the Date of the invoice
	 * @return The .pdf file containing the salary invoice
	 */
	private File createSalaryInvoice(String houseOfficialMail, Date invoiceDate) {
		String salaryInvoice = InvoiceHandler.createSalaryInvoice(invoiceDate);
		File texFile = FileHandler.createTexFile(Configuration.PATHTEXINVOICECUSTOMER + DateHandler.formatMonthDateToString(invoiceDate)
				+ "_LohnkostenAbrechnung.tex", salaryInvoice);
		File pdf = PdfHandler.createPdf(texFile);
		pdf.deleteOnExit();
		debug.debug("Salary invoice created");
		return pdf;
	}

	/**
	 * Helper method for creating a monthly invoice for each customer.
	 * <p>
	 * Creates the monthly invoice as .pdf file and writes it to the database.
	 * Deletes the temporary .pdf file when exiting the JVM.
	 * 
	 * @param houseOfficialMail Email adress of the "Haussprecher"
	 * @param operator Operator of the shop
	 * @param invoiceDate the Date of the invoice
	 * @param password
	 * @throws SQLException
	 */
	private void createCustomerInvoice(String houseOfficialMail, Staff operator, Date invoiceDate, String password) throws SQLException {
		String dateOfInvoiceToSend = DateHandler.formatMonthDateToString(invoiceDate);
		for (Customer c : tableControl.getCustomerMap().values()) {
			String tex = InvoiceHandler.createInvoiceCustomer(c, operator, invoiceDate);
			String path = Configuration.PATHTEXINVOICECUSTOMER + c.getName() + "_"
					+ DateHandler.formatMonthDateToString(DateHandler.getMonthBeforeDate(new Date())) + ".tex";
			File pdf = PdfHandler.createPdf(FileHandler.createTexFile(path, tex));
			int billID = QueryFactory.getBillID(c.getCustomerID(), DateHandler.formatMonthDateToString(DateHandler.getMonthBeforeDate(new Date())));
			double amount = QueryFactory.getTotalSumOfBill(billID);

			Status status = null;
			if (c.getPayment().equals(PaymentType.CASH)) {
				status = Status.PAID;
			} else {
				status = Status.ACTIVE;
			}

			QueryFactory.writeInvoiceCustomerToDatabase(billID, pdf, status, amount);
			pdf.deleteOnExit();

			String username = QueryFactory.getProperty(Property.USERNAMEEMAIL);
			if (amount != 0) {
				String email = c.getPerson().getEmail();
				// TODO change recipient, change pw
				EmailHandler.sendInvoiceMail(houseOfficialMail, operator.getPerson().getFirstName() + " " + operator.getPerson().getLastName(), Utility.concatStrings(c.getPerson().getFirstName()," ",c
						.getPerson().getLastName()), c.getPerson().getEmail(), password, username, DateHandler.formatHeaderDate(DateHandler
						.parseMonthDateStringToDate(dateOfInvoiceToSend)), pdf);
			}
		}
	}

//	/**
//	 * Helper method for sending an email containing the monthly invoice to a
//	 * customer.
//	 * 
//	 * @param houseOfficialMail Email address of the "Haussprecher"
//	 * @param operator Operator of the shop
//	 * @param enteredPassword password that was entered for sending the email
//	 * @param dateOfInvoiceToSend
//	 */
//	private void sendInvoiceEmail(String houseOfficialMail, Staff operator, String enteredPassword, String dateOfInvoiceToSend) {
//		String username = QueryFactory.getProperty(Property.USERNAMEEMAIL);
//
//		for (Customer c : tableControl.getCustomerMap().values()) {
//			int billID = QueryFactory.getBillID(c.getCustomerID(), dateOfInvoiceToSend);
//			if (QueryFactory.getTotalSumOfBill(billID) != 0) {
//				File invoiceFile = QueryFactory.getInvoiceFile(dateOfInvoiceToSend, billID);
//				String email = c.getPerson().getEmail();
//				// TODO change recipient, change pw
//				EmailHandler.sendInvoiceMail("matthiasikg@gmx.net", operator.getPerson().getFirstName() + " " + operator.getPerson().getLastName(), c
//						.getPerson().getLastName(), "matthiasikg@gmx.net", enteredPassword, username, DateHandler.formatHeaderDate(DateHandler
//						.parseMonthDateStringToDate(dateOfInvoiceToSend)), invoiceFile);
//			}
//		}
//
//	}

	/**
	 * Calculates the loss of one product.
	 * <p>
	 * If there is a difference between the theoretical stock and the actual
	 * stock the missing items will be calculated with the active product.
	 * 
	 * @param actualTrayCount
	 * @param theoreticalTrayCount
	 * @param sb StringBuilder to append the row for the inventory of this
	 *            product
	 * @param prod Product which is checked
	 * @param actualStock The stock that was saved from the inventory
	 * @return total loss of this product including all replacements
	 */
	private double calculateSingleProductLoss(StringBuilder sb, Product prod, int actualStock) {
		double loss = 0.0;

		if (prod != null && prod.getStock() < actualStock) {
			// Stock of all replacement products
			int stockOfReplacements = 0;
			if(prod.getReplacement() != null){stockOfReplacements = prod.getReplacement().getAvailability();}
			// Stock that this product should have
			int shouldStock = actualStock - stockOfReplacements;
			// Difference between the theoretical value and the actual value
			int difference = prod.getStock() - shouldStock;
			// Loss the difference of this product made
			double singleLoss = -1 * difference * prod.getCost();

			sb.append(InvoiceHandler.getInventoryProduct(shouldStock, prod.getStock(), prod.getName(), prod.getCost(), singleLoss));
			prod.setStock(shouldStock);
			loss += singleLoss;
			loss += calculateSingleProductLoss(sb, prod.getReplacement(), actualStock - shouldStock);
		} else {
			int difference = actualStock - prod.getStock();
			double singleLoss = difference * prod.getCost();
			sb.append(InvoiceHandler.getInventoryProduct(actualStock, prod.getStock(), prod.getName(), prod.getCost(), singleLoss));
			prod.setStock(actualStock);
			return singleLoss;
		}

		return loss;
	}

	/*
	 * GETTER AND SETTER BELOW
	 */

	/**
	 * Returns a list of all bills that were not paid yet
	 * 
	 * @return
	 */
	public List<Bill> getActiveDebts() {
		List<Bill> activeDebts = new ArrayList<>();

		for (Customer c : tableControl.getCustomerMap().values()) {
			Bill b = QueryFactory.getActiveDebt(c, null);
			if (b != null) {
				activeDebts.add(b);
			}
		}
		return activeDebts;
	}

	/**
	 * @param mostUpdated Flag if true then the latest replacement of a product
	 *            is added | if false then the product is added to the list
	 * @param type
	 * @return A list containing all active or waiting products depending on the
	 *         flag mostUpdated.
	 */
	public List<Product> getProducts(boolean mostUpdated, BoxType type) {
		List<Product> coll = new ArrayList<>();
		for (Product p : tableControl.getProductMap().values()) {
			if (p.getCategory().equals(type)) {
				if (mostUpdated) {
					coll.add(p.getLatestProduct());
				} else {
					coll.add(p);
				}
			}
		}
		Collections.sort(coll);
		return coll;
	}

	public Collection<Customer> getActiveCustomer() {
		return tableControl.getCustomerMap().values();
	}

	public Collection<Staff> getStaff() {
		return tableControl.getStaffMap().values();
	}

	public Customer getCustomer(String value) {
		return tableControl.getCustomerMap().get(value);
	}

	public Staff getWorkingStaff() {
		return tableControl.getActualSalesday().getStaff();
	}

	/**
	 * Deletes a given Staff instance from the instantiated Staff Map
	 * 
	 * @param s Staff to remove
	 */
	public void deleteStaff(Staff s) {
		tableControl.getStaffMap().remove(s.getStaffID(), s.getNickname());
	}

	public void deleteCustomer(Customer c) {
		tableControl.getCustomerMap().remove(c.getCustomerID(), c.getName());
	}

	public void addStaff(Staff s) {
		tableControl.getStaffMap().put(s.getStaffID(), s.getNickname(), s);
	}

	public void addProduct(Product newProduct) {
		tableControl.getProductMap().put(newProduct.getID(), newProduct.getName(), newProduct);
	}

	public void deleteProduct(Product replacement) {
		tableControl.getProductMap().remove(replacement.getID(), replacement.getName());
	}

	public void bookPurchase(ToPay toPay) {
		tableControl.bookPurchase(toPay);
	}

	public Map<String, Number> getRevenueData(boolean isRevenue, List<IFinishable> selectedCustomer, LocalDate begin, LocalDate end) {
		return QueryFactory.getRevenueData(isRevenue, selectedCustomer, begin, end);
	}

	public List<Product> getProductGroup(int groupID) {
		return QueryFactory.getProductGroup(groupID);
	}
}
